-- 
-- Please see the readme.txt file included with this distribution for 
-- attribution and copyright information.
--

enableglobaltoggle = true;
enablevisibilitytoggle = true;

ct_active_name = "";
aHostTargeting = {};

function onInit()
	Interface.onHotkeyActivated = onHotkey;

	-- Make sure all the clients can see the combat tracker
	for k,v in ipairs(User.getAllActiveIdentities()) do
		local username = User.getIdentityOwner(v);
		if username then
			NodeManager.addWatcher("combattracker", username);
			NodeManager.addWatcher("combattracker_props", username);
		end
	end

	-- Create a blank window if one doesn't exist already
	if not getNextWindow(nil) then
		NodeManager.createWindow(self);
	end

	-- Register a menu item to create a CT entry
	registerMenuItem("Create Item", "insert", 5);
	
	-- Rebuild targeting information
	rebuildClientTargeting();
	
	-- Initialize global buttons
	onVisibilityToggle();
	onEntrySectionToggle();
	
	-- Show global targeting toggle, if enabled
	if PremiumTargetingManager then
		window.button_global_targeting.setVisible(true);
		window.button_global_frame.setStaticBounds(-160,28,120,35);
	end
end

function onSortCompare(w1, w2)
	if w1.initresult.getValue() ~= w2.initresult.getValue() then
		return w1.initresult.getValue() < w2.initresult.getValue();
	end
		
	if w1.init.getValue() ~= w2.init.getValue() then
		return w1.init.getValue() < w2.init.getValue();
	end
	
	return w1.name.getValue() > w2.name.getValue();
end

function onMenuSelection(selection)
	if selection == 5 then
		NodeManager.createWindow(self);
	end
end

function onHotkey(draginfo)
	if draginfo.isType("combattrackernextactor") then
		nextActor();
		return true;
	end
	if draginfo.isType("combattrackernextround") then
		nextRound();
		return true;
	end
end

function getCTFromIdentity(sIdentity)
	local sIdentityLabel = User.getIdentityLabel(sIdentity);
	for k, v in pairs(getWindows()) do
		if v.type.getValue() == "pc" then
			if v.name.getValue() == sIdentityLabel then
				return v;
			end
		end
	end
	
	return nil;
end

function deleteTarget(sNode)
	TargetingManager.removeTargetFromAllEntries("host", sNode);
end

function rebuildClientTargeting()
	-- Start with the targeting data added by host
	local aClientTargets = {};

	-- Iterate through CT windows
	for keyCTEntry, winCTEntry in pairs(getWindows()) do
		-- Clear target list in window
		TargetingManager.clearTargets("client", winCTEntry.getDatabaseNode());
		
		-- Get targeting data from default client targeting support
		local instanceToken = winCTEntry.token.getReference();
		if instanceToken then
			local aTargeting = instanceToken.getTargetingIdentities();
			for i = #aTargeting, 1, -1 do
				local winTargetingCTEntry = getCTFromIdentity(aTargeting[i]);
				if winTargetingCTEntry then
					table.insert(aClientTargets, {nodeAttacker = winTargetingCTEntry.getDatabaseNode().getNodeName(), nodeDefender = winCTEntry.getDatabaseNode().getNodeName()});
				end
			end
		end
	end
	
	-- Using the target table, add target to windows
	for keyTarget, rTarget in pairs(aClientTargets) do
		TargetingManager.addTarget("client", rTarget.nodeAttacker, rTarget.nodeDefender);
	end
end

function toggleVisibility()
	if not enablevisibilitytoggle then
		return;
	end
	
	local visibilityon = window.button_global_visibility.getState();
	for k,v in pairs(getWindows()) do
		if v.type.getValue() ~= "pc" then
			if visibilityon ~= v.show_npc.getState() then
				v.show_npc.setState(visibilityon);
			end
		end
	end
end

function toggleTargeting()
	if not enableglobaltoggle then
		return;
	end
	
	local targetingon = window.button_global_targeting.getValue();
	for k,v in pairs(getWindows()) do
		if targetingon ~= v.activatetargeting.getValue() then
			v.activatetargeting.setValue(targetingon);
			v.setTargetingVisible(v.activatetargeting.getValue());
		end
	end
end

function toggleActive()
	if not enableglobaltoggle then
		return;
	end
	
	local activeon = window.button_global_active.getValue();
	for k,v in pairs(getWindows()) do
		if activeon ~= v.activateactive.getValue() then
			v.activateactive.setValue(activeon);
			v.setActiveVisible(v.activateactive.getValue());
		end
	end
end

function toggleDefensive()
	if not enableglobaltoggle then
		return;
	end
	
	local defensiveon = window.button_global_defensive.getValue();
	for k,v in pairs(getWindows()) do
		if defensiveon ~= v.activatedefensive.getValue() then
			v.activatedefensive.setValue(defensiveon);
			v.setDefensiveVisible(v.activatedefensive.getValue());
		end
	end
end

function toggleSpacing()
	if not enableglobaltoggle then
		return;
	end
	
	local spacingon = window.button_global_spacing.getValue();
	for k,v in pairs(getWindows()) do
		if spacingon ~= v.activatespacing.getValue() then
			v.activatespacing.setValue(spacingon);
			v.setSpacingVisible(v.activatespacing.getValue());
		end
	end
end

function toggleEffects()
	if not enableglobaltoggle then
		return;
	end
	
	local effectson = window.button_global_effects.getValue();
	for k,v in pairs(getWindows()) do
		if effectson ~= v.activateeffects.getValue() then
			v.activateeffects.setValue(effectson);
			v.setEffectsVisible(v.activateeffects.getValue());
			v.effects.checkForEmpty();
		end
	end
end

function onVisibilityToggle()
	local anyVisible = false;
	for k,v in pairs(getWindows()) do
		if v.type.getValue() ~= "pc" and v.show_npc.getState() then
			anyVisible = true;
		end
	end
	
	enablevisibilitytoggle = false;
	window.button_global_visibility.setState(anyVisible);
	enablevisibilitytoggle = true;
end

function onEntrySectionToggle()
	local anyTargeting = false;
	local anyActive = false;
	local anyDefensive = false;
	local anySpacing = false;
	local anyEffects = false;

	for k,v in pairs(getWindows()) do
		if v.activatetargeting.getValue() then
			anyTargeting = true;
		end
		if v.activatespacing.getValue() then
			anySpacing = true;
		end
		if v.activatedefensive.getValue() then
			anyDefensive = true;
		end
		if v.activateactive.getValue() then
			anyActive = true;
		end
		if v.activateeffects.getValue() then
			anyEffects = true;
		end
	end

	enableglobaltoggle = false;
	window.button_global_targeting.setValue(anyTargeting);
	window.button_global_active.setValue(anyActive);
	window.button_global_defensive.setValue(anyDefensive);
	window.button_global_spacing.setValue(anySpacing);
	window.button_global_effects.setValue(anyEffects);
	enableglobaltoggle = true;
end

function addPc(source)
	-- Parameter validation
	if not source then
		return nil;
	end

	-- Create a new combat tracker window
	local wnd = NodeManager.createWindow(self);
	if not wnd then
		return nil;
	end

	-- Shortcut
	wnd.link.setValue("charsheet", source.getNodeName());

	-- Type
	-- NOTE: Set to PC after link set, so that fields are linked correctly
	wnd.type.setValue("pc");

	-- Token
	local tokenval = NodeManager.get(source, "combattoken", nil);
	if tokenval then
		wnd.token.setPrototype(tokenval);
	end

	-- FoF
	wnd.friendfoe.setStringValue("friend");
	
	return wnd;
end

function addBattle(source)
	-- Parameter validation
	if not source then
		return nil;
	end

	-- Cycle through the NPC list, and add them to the tracker
	local npclistnode = source.getChild("npclist");
	if npclistnode then
		for k,v in pairs(npclistnode.getChildren()) do
			local n = NodeManager.get(v, "count", 0);
			for i = 1, n do
				if v.getChild("link") then
					local npcclass, npcnodename = v.getChild("link").getValue();
					local npcnode = DB.findNode(npcnodename);
					
					local wnd = addNpc(npcnode, NodeManager.get(v, "name", ""), NodeManager.get(v, "leveladj", 0));
					if wnd then
						local npctoken = NodeManager.get(v, "token", "");
						if npctoken ~= "" then
							wnd.token.setPrototype(npctoken);
						end
					else
						ChatManager.SystemMessage("Could not add '" .. NodeManager.get(v, "name", "") .. "' to combat tracker");
					end
				end
			end
		end
	end
end

function addNpc(source, name, leveladj)
	-- Parameter validation
	if not source then
		return nil;
	end
	if not leveladj then
		leveladj = 0;
	end

	-- Determine the options relevant to adding NPCs
	local opt_nnpc = OptionsManager.getOption("NNPC");
	local opt_init = OptionsManager.getOption("INIT");

	-- Create a new NPC window to hold the data
	local wnd = NodeManager.createWindow(self);
	if not wnd then
		return nil;
	end

	-- SETUP
	local base_effects = {};

	-- Shortcut
	wnd.link.setValue("npc", source.getNodeName());

	-- Type
	wnd.type.setValue("npc");

	-- Name
	local namelocal = name;
	if not namelocal then
		namelocal = NodeManager.get(source, "name", "");
	end
	local namecount = 0;
	local highnum = 0;
	local last_init = 0;
	wnd.name.setValue(namelocal);

	-- If multiple NPCs of same name, then figure out what initiative they go on and potentially append a number
	if string.len(namelocal) > 0 then
		for k, v in ipairs(getWindows()) do
			if wnd.name.getValue() == getWindows()[k].name.getValue() then
				namecount = 0;
				for l, w in ipairs(getWindows()) do
					local check = null;
					if getWindows()[l].name.getValue() == namelocal then
						check = 0;
					elseif string.sub(getWindows()[l].name.getValue(), 1, string.len(namelocal)) == namelocal then
						check = tonumber(string.sub(getWindows()[l].name.getValue(), string.len(namelocal)+2));
					end
					if check then
						namecount = namecount + 1;
						local cur_init = getWindows()[l].initresult.getValue();
						if cur_init ~= 0 then
							last_init = cur_init;
						end
						if highnum < check then
							highnum = check;
						end
					end
				end 
				if opt_nnpc == "append" then
					getWindows()[k].name.setValue(wnd.name.getValue().." "..highnum+1); 
				elseif opt_nnpc == "random" then
					getWindows()[k].name.setValue(randomName(getWindows(), wnd.name.getValue())); 
				end
			end
		end
	end
	if namecount < 2 then
        wnd.name.setValue(namelocal);
	end
	
	-- Space/reach
	local sType = NodeManager.get(source, "type", "");
	local wordsType = StringManager.parseWords(string.lower(sType));
	local space = 1;
	local reach = 1;
	if StringManager.isWord(wordsType[1], "tiny") then
		space = 1;
		reach = 0;
	elseif StringManager.isWord(wordsType[1], "small") then
		space = 1;
		reach = 1;
	elseif StringManager.isWord(wordsType[1], "medium") then
		space = 1;
		reach = 1;
	elseif StringManager.isWord(wordsType[1], "large") then
		space = 2;
		reach = 1;
	elseif StringManager.isWord(wordsType[1], "huge") then
		space = 3;
		reach = 2;
	elseif StringManager.isWord(wordsType[1], "gargantuan") then
		space = 4;
		reach = 3;
	end
	wnd.space.setValue(space);
	wnd.reach.setValue(reach);

	-- CHECK FOR SWARM
	local isSwarm = false;
	if StringManager.contains(wordsType, "swarm") then
		isSwarm = true;
	end
	
	-- Token
	local tokenval = NodeManager.get(source, "token", nil);
	if tokenval then
		wnd.token.setPrototype(tokenval);
	end
	
	-- FoF
	wnd.friendfoe.setStringValue("foe");
		
	-- Determine the NPC level
	local levelrole = string.lower(NodeManager.get(source, "levelrole", ""));
	local levelstr = string.match(levelrole, "level (%d*)");
	local levelval = tonumber(levelstr) or 0;

	-- HP
	local hp = NodeManager.get(source, "hp", "");
	local npchp = string.match(hp, "(%d*)");
	local numhp = tonumber(npchp) or 0;
	if leveladj ~= 0 then
		if string.match(levelrole, "minion") then
			-- Do nothing, since minions always get 1 hp
		elseif string.match(levelrole, "solo") and levelval > 0 then
			levelval = levelval + leveladj;
			local con = NodeManager.get(source, "constitution", 10);
			if levelval > 10 then
				numhp = (((levelval + 1) * 8) + con) * 5;
			else
				numhp = (((levelval + 1) * 8) + con) * 4;
			end
		else
			local hpadj = 8;
			local hpadjmult = 1;
			if string.match(levelrole, "brute") then
				hpadj = 10;
			elseif string.match(levelrole, "lurker") then
				hpadj = 6;
			elseif string.match(levelrole, "artillery") then
				hpadj = 6;
			end
			if string.match(levelrole, "elite") then
				hpadjmult = 2;
			elseif string.match(levelrole, "solo") then
				hpadjmult = 4;
			end
			numhp = numhp + (hpadj * hpadjmult * leveladj);
		end
	end
	wnd.hp.setValue(numhp);
	
	local healsurges = 1;
	if levelval > 20 then
		healsurges = 3;
	elseif levelval > 10 then
		healsurges = 2;
	end
	wnd.healsurgeremaining.setValue(healsurges);
	
	-- Defensive properties
	wnd.ac.setValue(NodeManager.get(source, "ac", 0));
	wnd.fortitude.setValue(NodeManager.get(source, "fortitude", 0));
	wnd.reflex.setValue(NodeManager.get(source, "reflex", 0));
	wnd.will.setValue(NodeManager.get(source, "will", 0));
	
	local sSpecialDefenses = NodeManager.get(source, "specialdefenses", "");
	local sSave = NodeManager.get(source, "saves", "");
	local nSave = 0;
	if StringManager.isNumberString(sSave) then
		 nSave = tonumber(sSave) or 0;
	elseif sSave ~= "" then
		if sSpecialDefenses ~= "" then
			sSpecialDefenses = "Save " .. sSave .. "; " .. sSpecialDefenses;
		else
			sSpecialDefenses = "Save " .. sSave;
		end
	end
	wnd.save.setValue(nSave);
	wnd.specialdef.setValue(sSpecialDefenses);
	
	local listSpecialClauses = StringManager.split(string.lower(wnd.specialdef.getValue()), ";", true);
	for keyClause, sClause in pairs(listSpecialClauses) do
		local wordsClause = StringManager.parseWords(sClause);
		if StringManager.isWord(wordsClause[1], "immune") then
			for i = 2, #wordsClause do
				if StringManager.isWord(wordsClause[i], DataCommon.dmgtypes) or
						StringManager.isWord(wordsClause[i], DataCommon.immunetypes) or
						StringManager.isWord(wordsClause[i], "all") then
					table.insert(base_effects, "IMMUNE: " .. wordsClause[i]);
				end
			end
		elseif StringManager.isWord(wordsClause[1], "resist") then
			for i = 2, #wordsClause do
				if StringManager.isWord(wordsClause[i], {"insubstantial"}) then
					table.insert(base_effects, "Insubstantial");
				elseif StringManager.isNumberString(wordsClause[i]) then
					local nTemp = tonumber(wordsClause[i]) or 0;
					if nTemp > 0 then
						if StringManager.isWord(wordsClause[i+1], DataCommon.dmgtypes) then
							table.insert(base_effects, "RESIST: " .. nTemp .. " " .. wordsClause[i+1]);
						elseif StringManager.isWord(wordsClause[i+2], "all") then
							table.insert(base_effects, "RESIST: " .. nTemp);
						end
					end
				end
			end
		elseif StringManager.isWord(wordsClause[1], "vulnerable") then
			for i = 2, #wordsClause do
				if StringManager.isNumberString(wordsClause[i]) then
					local nTemp = tonumber(wordsClause[i]) or 0;
					if nTemp > 0 then
						if StringManager.isWord(wordsClause[i+1], DataCommon.dmgtypes) then
							table.insert(base_effects, "VULN: " .. nTemp .. " " .. wordsClause[i+1]);
						elseif StringManager.isWord(wordsClause[i+2], "all") then
							table.insert(base_effects, "VULN: " .. nTemp);
						elseif isSwarm and StringManager.isWord(wordsClause[i+1], "against") then
							table.insert(base_effects, "SWARM: " .. nTemp);
						end
					end
				end
			end
		end
	end -- END SPECIAL DEFENSES PROCESSING

	-- Active properties
	wnd.init.setValue(NodeManager.get(source, "init", 0));
	
	local speed = NodeManager.get(source, "speed", "");
	local npcspeed = string.match(speed, "(%d*)");
	local numspeed = tonumber(npcspeed) or 0;
	wnd.speed.setValue(numspeed);
	if string.match(speed, "phasing") then
		table.insert(base_effects, "Phasing");
	end

	local ap = tonumber(NodeManager.get(source, "actionpoints", "")) or 0;
	wnd.actionpoints.setValue(ap);

	if source.getChild("powers") then
		for k, v in pairs(wnd.attacks.getWindows()) do
			v.getDatabaseNode().delete();
		end
		
		local powerchildren = source.getChild("powers").getChildren();
		local numpowers = 0;
		for key, val in pairs(powerchildren) do
			numpowers = numpowers + 1;
		end
		local powercount = 0;
		local k = 1;
		while powercount < numpowers do
			local v = powerchildren["id-" .. string.format("%05d", k)];
			
			if v then
				powercount = powercount + 1;
				local powerstr = "";

				-- Pull the main clause to parse
				local main_clause = NodeManager.get(v, "shortdescription", "");

				-- Get the attack, damage and effect clauses
				local abilities = PowersManager.parsePowerDescription(v, NodeManager.get(source, "name", ""));
				local attack_count = 0;
				local damage_count = 0;
				local heal_count = 0;
				local effect_count = 0;
				local special_attack_flag = false;
				local special_damage_flag = false;
				for i = 1, #abilities do
					if abilities[i].type == "attack" then
						attack_count = attack_count + 1;
						if (attack_count > 1) or 
								(#(abilities[i].clauses) ~= 1) or
								(#(abilities[i].clauses[1].stat) > 0) then
							special_attack_flag = true;
						else
							local sDefense = abilities[i].defense;
							if sDefense == "ac" then
								sDefense = "AC";
							elseif sDefense == "reflex" then
								sDefense = "Ref";
							elseif sDefense == "will" then
								sDefense = "Will";
							elseif sDefense == "fortitude" then
								sDefense = "Fort";
							else
								sDefense = "-";
							end

							powerstr = powerstr .. string.format("(%+d vs. %s)", abilities[i].clauses[1].mod, sDefense);
						end
					elseif abilities[i].type == "damage" then
						damage_count = damage_count + 1;
						if (damage_count > 1) or 
								(#(abilities[i].clauses) ~= 1) or
								(#(abilities[i].clauses[1].stat) > 0) or 
								(abilities[i].clauses[1].dicestr == "") or 
								(abilities[i].clauses[1].basemult ~= 0) or
								(abilities[i].clauses[1].critdicestr ~= "") then
							special_damage_flag = true;
						else
							local sType = "";
							if abilities[i].clauses[1].subtype ~= "" then
								sType = " " .. abilities[i].clauses[1].subtype;
							end
							
							powerstr = powerstr .. string.format("(%s%s)", abilities[i].clauses[1].dicestr, sType);
						end
					elseif abilities[i].type == "heal" then
						heal_count = heal_count + 1;
					elseif abilities[i].type == "effect" then
						effect_count = effect_count + 1;
					end
				end
				
				if special_attack_flag then
					powerstr = powerstr .. "[SATK]";
				end
				if special_damage_flag then
					powerstr = powerstr .. "[SDMG]";
				end
				if heal_count > 0 then
					powerstr = powerstr .. "[HEAL]";
				end
				if effect_count > 0 then
					powerstr = powerstr .. "[EFF]";
				end

				-- Determine the range of this power
				local powertype = string.lower(NodeManager.get(v, "powertype", ""));
				if StringManager.contains({"m", "r", "c", "a", "mr", "mc", "ma", "rc", "ra", "ca"}, powertype) then
					powerstr = powerstr .. "[" .. string.upper(powertype) .. "]";
				end
				
				-- Determine the action required to use this power, and add a note if it is not standard
				local action = string.lower(NodeManager.get(v, "action", ""));
				if action == "move" then
					powerstr = powerstr .. "[mo]";
				elseif action == "minor" then
					powerstr = powerstr .. "[mi]";
				elseif action == "reaction" or action == "immediate reaction" then
					powerstr = powerstr .. "[r]";
				elseif action == "interrupt" or action == "immediate interrupt" then
					powerstr = powerstr .. "[i]";
				elseif action == "aura" then
					local aura_range = tonumber(NodeManager.get(v, "range", "")) or 0;
					if aura_range > 0 then
						powerstr = powerstr .. "[AURA:" .. aura_range .. "]";
					end
				end

				-- Determine the recharge of this power, and add a note if it is not at-will
				local recharge = string.lower(NodeManager.get(v, "recharge", ""));
				if recharge == "encounter" then
					powerstr = powerstr .. "[e]";
				elseif recharge == "daily" then
					powerstr = powerstr .. "[d]";
				elseif string.sub(recharge,1,8) == "recharge" then
					powerstr = powerstr .. "[R:" .. string.sub(recharge,10,10) .. "]";
				end

				-- Add the power name
				local powername = NodeManager.get(v, "name", "");
				if powerstr ~= "" then
					powerstr = " " .. powerstr;
				end
				powerstr = powername .. powerstr;

				local powertype = NodeManager.get(v, "powertype", "");
				if powertype == "m" or powertype == "r" or powertype == "c" then
					powerstr = "*" .. powerstr;
				end

				-- Add this attack to the combat tracker power string
				local wndAttack  = wnd.attacks.createWindow();
				wndAttack.value.setValue(powerstr);
				
				-- Special handling for threathening reach ability
				if powername == "Threatening Reach" then
					local reachstr = string.match(main_clause, "reach %((%d) squares%)");
					local reachnum = tonumber(reachstr) or 0;
					if reachnum > wnd.reach.getValue() then
						wnd.reach.setValue(reachnum);
 					end
				end
			end
			
			-- Cycle to the next power
			k = k + 1;
		end

		if #(wnd.attacks.getWindows()) == 0 then
			wnd.attacks.createWindow();
		end
	end
	
	-- If the NPC has regeneration then, add an effect for it
	local regen_val = string.match(wnd.specialdef.getValue(), "Regeneration (%d+)");
	if regen_val then
		EffectsManager.addEffect("", "", wnd.getDatabaseNode(), { sName = "REGEN: " .. regen_val, nGMOnly = 1 });
	end
	
	-- If there is a level adjustment, then add an effect for it
	if leveladj ~= 0 then
		local dmg_adj = 0;
		if leveladj > 0 then
			dmg_adj = math.floor(leveladj / 2);
		else
			dmg_adj = math.ceil(leveladj / 2);
		end
		
		local eff_text = "DEF:" .. leveladj .. "; ATK:" .. leveladj;
		if dmg_adj ~= 0 then
			eff_text = eff_text .. "; DMG:" .. dmg_adj;
		end
		EffectsManager.addEffect("", "", wnd.getDatabaseNode(), { sName = eff_text, nGMOnly = 1 });
	end
	
	-- If there are any effects to add, then add them
	if #base_effects > 0 then
		EffectsManager.addEffect("", "", wnd.getDatabaseNode(), { sName = table.concat(base_effects, "; "), nGMOnly = 1 });
	end

	--Roll initiative and sort
	if opt_init == "group" then
		if (namecount < 2) or (last_init == 0) then
			wnd.initresult.setValue(math.random(20) + wnd.init.getValue());
		else
			wnd.initresult.setValue(last_init);
		end
		applySort();
	elseif opt_init == "on" then
		wnd.initresult.setValue(math.random(20) + wnd.init.getValue());
		applySort();
	end

	return wnd;
end

function onDrop(x, y, draginfo)
	-- Capture certain drag types meant for the host only
	local dragtype = draginfo.getType();

	-- PC
	if dragtype == "playercharacter" then
		addPc(draginfo.getDatabaseNode());
		rebuildClientTargeting();
		return true;
	end

	if dragtype == "shortcut" then
		local class, datasource = draginfo.getShortcutData();

		-- NPC
		if class == "npc" then
			addNpc(draginfo.getDatabaseNode());
			return true;
		end

		-- ENCOUNTER
		if class == "battle" then
			addBattle(draginfo.getDatabaseNode());
			return true;
		end
	end

	-- Capture any drops meant for specific CT entries
	local wnd = getWindowAt(x,y);
	if wnd then
		return CombatCommon.onDrop("ct", wnd.getDatabaseNode().getNodeName(), draginfo);
	end
end

function getActiveEntry()
	for k, v in ipairs(getWindows()) do
		if v.isActive() then
			return v;
		end
	end
	
	return nil;
end

function requestActivation(entry)
	-- Make all the CT entries inactive
	for k, v in ipairs(getWindows()) do
		v.setActive(false);
	end
	
	-- Make the given CT entry active
	entry.setActive(true);

	-- Clear the immediate action checkmark, since its a new round
	entry.immediate_check.setState(false);
	
	-- If we created a new speaker, then remove it
	if ct_active_name ~= "" then
		GmIdentityManager.removeIdentity(ct_active_name);
		ct_active_name = "";
	end

	-- Check the option to set the active CT as the GM voice
	if OptionsManager.isOption("CTAV", "on") then
		-- Set up the current CT entry as the speaker if NPC, otherwise just change the GM voice
		if entry.type.getValue() == "pc" then
			GmIdentityManager.activateGMIdentity();
		else
			local name = entry.name.getValue();
			if GmIdentityManager.existsIdentity(name) then
				GmIdentityManager.setCurrent(name);
			else
				ct_active_name = name;
				GmIdentityManager.addIdentity(name);
			end
		end
	end
end

function nextActor()
	local active = getActiveEntry();
	if active then
		--- Process dying state for this actor first (PC only)
		if active.hp.getValue() > 0 and active.wounds.getValue() >= active.hp.getValue() then
			if OptionsManager.isOption("ESAV", "on") then
				local rActor = CombatCommon.getActor("ct", active.getDatabaseNode());
				if rActor.sType == "pc" and rActor.nodeCreature then
					local rActor, rSave = CombatCommon.getSaveRollStructures("pc", rActor.nodeCreature);
					local save_name, save_dice, save_mod = RulesManager.buildSaveRoll(rActor, rSave, 1);
					RulesManager.dclkAction("autosave", save_mod, save_name, rActor, nil, save_dice, true);
				end
			end
		end
	end
	
	-- Find the next actor.  If no next actor, then start the next round
	local nextactor = getNextWindow(active);
	if nextactor then
		if active then
			EffectsManager.processEffects(getDatabaseNode(), active.getDatabaseNode(), nextactor.getDatabaseNode());
		else
			EffectsManager.processEffects(getDatabaseNode(), nil, nextactor.getDatabaseNode());
		end
		requestActivation(nextactor);
	else
		nextRound();
	end
end

function nextRound()
	-- IF ACTIVE ACTOR, THEN PROCESS EFFECTS
	local active = getActiveEntry();
	if active then
		EffectsManager.processEffects(getDatabaseNode(), active.getDatabaseNode(), nil);
		active.setActive(false);
	end

	-- ADVANCE ROUND COUNTER
	window.roundcounter.setValue(window.roundcounter.getValue() + 1);
	
	-- ANNOUNCE NEW ROUND
	local msg = {font = "narratorfont", icon = "indicator_flag"};
	msg.text = "[ROUND " .. window.roundcounter.getValue() .. "]";
	ChatManager.deliverMessage(msg);
	
	-- CHECK OPTION TO SEE IF WE SHOULD GO AHEAD AND MOVE TO FIRST ROUND
	if OptionsManager.isOption("RNDS", "off") and getNextWindow(nil) then
		nextActor();
	end
end

function stripCreatureNumber(s)
	local starts, ends, creature_number = string.find(s, " ?(%d+)$");
	if not starts then
		return s;
	end
	return string.sub(s, 1, starts), creature_number;
end

function rollEntryInit(ctentry)
	-- Start with the bsae initiative bonus
	local ctinitval = ctentry.init.getValue();
	
	-- Get any effect modifiers
	local eff_dice, eff_bonus = EffectsManager.getEffectsBonus(ctentry.getDatabaseNode(), "INIT");
	ctinitval = ctinitval + eff_bonus;
	for k, v in pairs(eff_dice) do
		local die = tonumber(string.sub(v, 2)) or 0;
		if die > 0 then
			ctinitval = ctinitval + math.random(die);
		end
	end
	
	-- For PCs, we always roll unique initiative
	if ctentry.type.getValue() == "pc" then
		ctentry.initresult.setValue(math.random(20) + ctinitval);
		return;
	end
	
	-- For NPCs, if NPC init option is not group, then roll unique initiative
	local opt_init = OptionsManager.getOption("INIT");
	if opt_init ~= "group" then
		ctentry.initresult.setValue(math.random(20) + ctinitval);
		return;
	end

	-- For NPCs with group option enabled
	
	-- Get the entry's database node name and creature name
	local ctentrynodename = ctentry.getDatabaseNode().getNodeName();
	local ctentryname = stripCreatureNumber(ctentry.name.getValue());
	if ctentryname == "" then
		ctentry.initresult.setValue(math.random(20) + ctinitval);
		return;
	end
		
	-- Iterate through list looking for other creature's with same name
	local last_init = 0;
	for k,v in pairs(getWindows()) do
		if ctentrynodename ~= v.getDatabaseNode().getNodeName() then
			local tempentryname = stripCreatureNumber(v.name.getValue());
			if tempentryname == ctentryname then
				local cur_init = v.initresult.getValue();
				if cur_init ~= 0 then
					last_init = cur_init;
				end
			end
			
		end
	end
	
	-- If we found similar creatures with non-zero initiatives, then match the initiative of the last one found
	if last_init == 0 then
		ctentry.initresult.setValue(math.random(20) + ctinitval);
	else
		ctentry.initresult.setValue(last_init);
	end
end

function rollAllInit()
	for k, v in ipairs(getWindows()) do
		if v.type.getValue() == "npc" then
			v.initresult.setValue(0);
		end
	end

	for k, v in ipairs(getWindows()) do
		rollEntryInit(v);
	end
end

function rollPCInit()
	for k, v in ipairs(getWindows()) do
		if v.type.getValue() == "pc" then
			rollEntryInit(v);
		end
	end
end

function rollNPCInit()
	for k, v in ipairs(getWindows()) do
		if v.type.getValue() == "npc" then
			v.initresult.setValue(0);
		end
	end

	for k, v in ipairs(getWindows()) do
		if v.type.getValue() == "npc" then
			rollEntryInit(v);
		end
	end
end

function resetInit()
	-- Set all CT entries to inactive and reset their init value
	for k, v in ipairs(getWindows()) do
		v.setActive(false);
		v.initresult.setValue(0);
		v.immediate_check.setState(false);
	end
	
	-- Remove the active CT from the speaker list
	if ct_active_name ~= "" then
		GmIdentityManager.removeIdentity(ct_active_name);
		ct_active_name = "";
	end

	-- Reset the round counter
	window.roundcounter.setValue(1);
end

function rest(extendedflag, milestoneflag)
	resetInit();
	clearExpiringEffects();

	for k, v in ipairs(getWindows()) do
		if v.type.getValue() == "pc" then
			local vnode = v.link.getTargetDatabaseNode();
			if vnode then
				CharSheetCommon.rest(vnode, extendedflag, milestoneflag);
			end
		end
	end
end

function clearExpiringEffects()
	for k, v in ipairs(getWindows()) do
		-- Clear any effects that have an expiration value
		local effcount = #(v.effects.getWindows());
		for k2, v2 in ipairs(v.effects.getWindows()) do
			if v2.expiration.getStringValue() ~= "" or v2.apply.getStringValue() ~= "" or v2.label.getValue() == "" then
				v.effects.deleteChild(v2, false);
				effcount = effcount - 1;
			end
		end
		
		-- If no effects left, then clear the effects completely
		if effcount == 0 then
			v.effects.checkForEmpty();
			v.activateeffects.setValue(false);
			v.setEffectsVisible(false);
		end
	end
	
	-- Synch the global effects toggle
	onEntrySectionToggle();
end

function resetEffects()
	for k, v in ipairs(getWindows()) do
		-- Delete all current effects
		v.effects.reset(true);

		-- Hide the effects sub-section
		v.activateeffects.setValue(false);
		v.setEffectsVisible(false);
	end
	
	-- Synch the global effects toggle
	onEntrySectionToggle();
end

function deleteNPCs()
	for k, v in ipairs(getWindows()) do
		if v.type.getValue() == "npc" then
			v.delete();
		end
	end
end

function randomName(wintable, base_name)
	local new_name = base_name .. " " .. math.random(#wintable * 2) + 1	
	for l, w in ipairs(wintable) do
		if wintable[l].name.getValue() == new_name then
			new_name = randomName(wintable,base_name);
		end
	end
	return new_name
end
